// Parsing utility functions

import check from './check.mjs';

// Retrieve an unsigned byte from the DataView.
function getByte(dataView, offset) {
    return dataView.getUint8(offset);
}

// Retrieve an unsigned 16-bit short from the DataView.
// The value is stored in big endian.
function getUShort(dataView, offset) {
    return dataView.getUint16(offset, false);
}

// Retrieve a signed 16-bit short from the DataView.
// The value is stored in big endian.
function getShort(dataView, offset) {
    return dataView.getInt16(offset, false);
}

function getUInt24(dataView, offset) {
    return (dataView.getUint16(offset) << 8) + dataView.getUint8(offset + 2);
}

// Retrieve an unsigned 32-bit long from the DataView.
// The value is stored in big endian.
function getULong(dataView, offset) {
    return dataView.getUint32(offset, false);
}

// Retrieve a signed 32-bit long from the DataView.
// The value is stored in big endian.
function getLong(dataView, offset) {
    return dataView.getInt32(offset, false);
}


// Retrieve a 32-bit signed fixed-point number (16.16) from the DataView.
// The value is stored in big endian.
function getFixed(dataView, offset) {
    const decimal = dataView.getInt16(offset, false);
    const fraction = dataView.getUint16(offset + 2, false);
    return decimal + fraction / 65535;
}

// Retrieve a 4-character tag from the DataView.
// Tags are used to identify tables.
function getTag(dataView, offset) {
    let tag = '';
    for (let i = offset; i < offset + 4; i += 1) {
        tag += String.fromCharCode(dataView.getInt8(i));
    }

    return tag;
}

// Retrieve an offset from the DataView.
// Offsets are 1 to 4 bytes in length, depending on the offSize argument.
function getOffset(dataView, offset, offSize) {
    let v = 0;
    for (let i = 0; i < offSize; i += 1) {
        v <<= 8;
        v += dataView.getUint8(offset + i);
    }

    return v;
}

// Retrieve a number of bytes from start offset to the end offset from the DataView.
function getBytes(dataView, startOffset, endOffset) {
    const bytes = [];
    for (let i = startOffset; i < endOffset; i += 1) {
        bytes.push(dataView.getUint8(i));
    }

    return bytes;
}

// Convert the list of bytes to a string.
function bytesToString(bytes) {
    let s = '';
    for (let i = 0; i < bytes.length; i += 1) {
        s += String.fromCharCode(bytes[i]);
    }

    return s;
}

const typeOffsets = {
    byte: 1,
    uShort: 2,
    f2dot14: 2,
    short: 2,
    uInt24: 3,
    uLong: 4,
    fixed: 4,
    longDateTime: 8,
    tag: 4
};

const masks = {
    LONG_WORDS: 0x8000,
    WORD_DELTA_COUNT_MASK: 0x7FFF,
    SHARED_POINT_NUMBERS: 0x8000,
    COUNT_MASK: 0x0FFF,
    EMBEDDED_PEAK_TUPLE: 0x8000,
    INTERMEDIATE_REGION: 0x4000,
    PRIVATE_POINT_NUMBERS: 0x2000,
    TUPLE_INDEX_MASK: 0x0FFF,
    POINTS_ARE_WORDS: 0x80,
    POINT_RUN_COUNT_MASK: 0x7F,
    DELTAS_ARE_ZERO: 0x80,
    DELTAS_ARE_WORDS: 0x40,
    DELTA_RUN_COUNT_MASK: 0x3F,
    INNER_INDEX_BIT_COUNT_MASK: 0x0F,
    MAP_ENTRY_SIZE_MASK: 0x30,
};

// A stateful parser that changes the offset whenever a value is retrieved.
// The data is a DataView.
function Parser(data, offset) {
    this.data = data;
    this.offset = offset;
    this.relativeOffset = 0;
}

Parser.prototype.parseByte = function() {
    const v = this.data.getUint8(this.offset + this.relativeOffset);
    this.relativeOffset += 1;
    return v;
};

Parser.prototype.parseChar = function() {
    const v = this.data.getInt8(this.offset + this.relativeOffset);
    this.relativeOffset += 1;
    return v;
};

Parser.prototype.parseCard8 = Parser.prototype.parseByte;

Parser.prototype.parseUShort = function() {
    const v = this.data.getUint16(this.offset + this.relativeOffset);
    this.relativeOffset += 2;
    return v;
};

Parser.prototype.parseCard16 = Parser.prototype.parseUShort;
Parser.prototype.parseSID = Parser.prototype.parseUShort;
Parser.prototype.parseOffset16 = Parser.prototype.parseUShort;

Parser.prototype.parseShort = function() {
    const v = this.data.getInt16(this.offset + this.relativeOffset);
    this.relativeOffset += 2;
    return v;
};

Parser.prototype.parseF2Dot14 = function() {
    const v = this.data.getInt16(this.offset + this.relativeOffset) / 16384;
    this.relativeOffset += 2;
    return v;
};


Parser.prototype.parseUInt24 = function() {
    const v = getUInt24(this.data, this.offset + this.relativeOffset);
    this.relativeOffset += 3;
    return v;
};

Parser.prototype.parseULong = function() {
    const v = getULong(this.data, this.offset + this.relativeOffset);
    this.relativeOffset += 4;
    return v;
};

Parser.prototype.parseLong = function() {
    const v = getLong(this.data, this.offset + this.relativeOffset);
    this.relativeOffset += 4;
    return v;
};

Parser.prototype.parseOffset32 = Parser.prototype.parseULong;

Parser.prototype.parseFixed = function() {
    const v = getFixed(this.data, this.offset + this.relativeOffset);
    this.relativeOffset += 4;
    return v;
};

Parser.prototype.parseString = function(length) {
    const dataView = this.data;
    const offset = this.offset + this.relativeOffset;
    let string = '';
    this.relativeOffset += length;
    for (let i = 0; i < length; i++) {
        string += String.fromCharCode(dataView.getUint8(offset + i));
    }

    return string;
};

Parser.prototype.parseTag = function() {
    return this.parseString(4);
};

// LONGDATETIME is a 64-bit integer.
// JavaScript and unix timestamps traditionally use 32 bits, so we
// only take the last 32 bits.
// + Since until 2038 those bits will be filled by zeros we can ignore them.
// FIXME: at some point we need to support dates >2038 using the full 64bit
Parser.prototype.parseLongDateTime = function() {
    let v = getULong(this.data, this.offset + this.relativeOffset + 4);
    // Subtract seconds between 01/01/1904 and 01/01/1970
    // to convert Apple Mac timestamp to Standard Unix timestamp
    v -= 2082844800;
    this.relativeOffset += 8;
    return v;
};

Parser.prototype.parseVersion = function(minorBase) {
    const major = getUShort(this.data, this.offset + this.relativeOffset);

    // How to interpret the minor version is very vague in the spec. 0x5000 is 5, 0x1000 is 1
    // Default returns the correct number if minor = 0xN000 where N is 0-9
    // Set minorBase to 1 for tables that use minor = N where N is 0-9
    const minor = getUShort(this.data, this.offset + this.relativeOffset + 2);
    this.relativeOffset += 4;
    if (minorBase === undefined) minorBase = 0x1000;
    return major + minor / minorBase / 10;
};

Parser.prototype.skip = function(type, amount) {
    if (amount === undefined) {
        amount = 1;
    }

    this.relativeOffset += typeOffsets[type] * amount;
};

///// Parsing lists and records ///////////////////////////////

// Parse a list of 32 bit unsigned integers.
Parser.prototype.parseULongList = function(count) {
    if (count === undefined) { count = this.parseULong(); }
    const offsets = new Array(count);
    const dataView = this.data;
    let offset = this.offset + this.relativeOffset;
    for (let i = 0; i < count; i++) {
        offsets[i] = dataView.getUint32(offset);
        offset += 4;
    }

    this.relativeOffset += count * 4;
    return offsets;
};

// Parse a list of 16 bit unsigned integers. The length of the list can be read on the stream
// or provided as an argument.
Parser.prototype.parseOffset16List =
Parser.prototype.parseUShortList = function(count) {
    if (count === undefined) { count = this.parseUShort(); }
    const offsets = new Array(count);
    const dataView = this.data;
    let offset = this.offset + this.relativeOffset;
    for (let i = 0; i < count; i++) {
        offsets[i] = dataView.getUint16(offset);
        offset += 2;
    }

    this.relativeOffset += count * 2;
    return offsets;
};

// Parses a list of 16 bit signed integers.
Parser.prototype.parseShortList = function(count) {
    const list = new Array(count);
    const dataView = this.data;
    let offset = this.offset + this.relativeOffset;
    for (let i = 0; i < count; i++) {
        list[i] = dataView.getInt16(offset);
        offset += 2;
    }

    this.relativeOffset += count * 2;
    return list;
};

// Parses a list of bytes.
Parser.prototype.parseByteList = function(count) {
    const list = new Array(count);
    const dataView = this.data;
    let offset = this.offset + this.relativeOffset;
    for (let i = 0; i < count; i++) {
        list[i] = dataView.getUint8(offset++);
    }

    this.relativeOffset += count;
    return list;
};

/**
 * Parse a list of items.
 * Record count is optional, if omitted it is read from the stream.
 * itemCallback is one of the Parser methods.
 */
Parser.prototype.parseList = function(count, itemCallback) {
    if (!itemCallback) {
        itemCallback = count;
        count = this.parseUShort();
    }
    const list = new Array(count);
    for (let i = 0; i < count; i++) {
        list[i] = itemCallback.call(this);
    }
    return list;
};

Parser.prototype.parseList32 = function(count, itemCallback) {
    if (!itemCallback) {
        itemCallback = count;
        count = this.parseULong();
    }
    const list = new Array(count);
    for (let i = 0; i < count; i++) {
        list[i] = itemCallback.call(this);
    }
    return list;
};

/**
 * Parse a list of records.
 * Record count is optional, if omitted it is read from the stream.
 * Example of recordDescription: { sequenceIndex: Parser.uShort, lookupListIndex: Parser.uShort }
 */
Parser.prototype.parseRecordList = function(count, recordDescription) {
    // If the count argument is absent, read it in the stream.
    if (!recordDescription) {
        recordDescription = count;
        count = this.parseUShort();
    }
    const records = new Array(count);
    const fields = Object.keys(recordDescription);
    for (let i = 0; i < count; i++) {
        const rec = {};
        for (let j = 0; j < fields.length; j++) {
            const fieldName = fields[j];
            const fieldType = recordDescription[fieldName];
            rec[fieldName] = fieldType.call(this);
        }
        records[i] = rec;
    }
    return records;
};

Parser.prototype.parseRecordList32 = function(count, recordDescription) {
    // If the count argument is absent, read it in the stream.
    if (!recordDescription) {
        recordDescription = count;
        count = this.parseULong();
    }
    const records = new Array(count);
    const fields = Object.keys(recordDescription);
    for (let i = 0; i < count; i++) {
        const rec = {};
        for (let j = 0; j < fields.length; j++) {
            const fieldName = fields[j];
            const fieldType = recordDescription[fieldName];
            rec[fieldName] = fieldType.call(this);
        }
        records[i] = rec;
    }
    return records;
};

Parser.prototype.parseTupleRecords = function(tupleCount, axisCount) {
    let tuples = [];
    for (let i = 0; i < tupleCount; i++) {
        let tuple = [];
        for (let axisIndex = 0; axisIndex < axisCount; axisIndex++) {
            tuple.push(this.parseF2Dot14());
        }
        tuples.push(tuple);
    }
    return tuples;
};

// Parse a data structure into an object
// Example of description: { sequenceIndex: Parser.uShort, lookupListIndex: Parser.uShort }
Parser.prototype.parseStruct = function(description) {
    if (typeof description === 'function') {
        return description.call(this);
    } else {
        const fields = Object.keys(description);
        const struct = {};
        for (let j = 0; j < fields.length; j++) {
            const fieldName = fields[j];
            const fieldType = description[fieldName];
            struct[fieldName] = fieldType.call(this);
        }
        return struct;
    }
};

/**
 * Parse a GPOS valueRecord
 * https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#value-record
 * valueFormat is optional, if omitted it is read from the stream.
 */
Parser.prototype.parseValueRecord = function(valueFormat) {
    if (valueFormat === undefined) {
        valueFormat = this.parseUShort();
    }
    if (valueFormat === 0) {
        // valueFormat2 in kerning pairs is most often 0
        // in this case return undefined instead of an empty object, to save space
        return;
    }
    const valueRecord = {};

    if (valueFormat & 0x0001) { valueRecord.xPlacement = this.parseShort(); }
    if (valueFormat & 0x0002) { valueRecord.yPlacement = this.parseShort(); }
    if (valueFormat & 0x0004) { valueRecord.xAdvance = this.parseShort(); }
    if (valueFormat & 0x0008) { valueRecord.yAdvance = this.parseShort(); }

    // Device table (non-variable font) / VariationIndex table (variable font) not supported
    // https://docs.microsoft.com/fr-fr/typography/opentype/spec/chapter2#devVarIdxTbls
    if (valueFormat & 0x0010) { valueRecord.xPlaDevice = undefined; this.parseShort(); }
    if (valueFormat & 0x0020) { valueRecord.yPlaDevice = undefined; this.parseShort(); }
    if (valueFormat & 0x0040) { valueRecord.xAdvDevice = undefined; this.parseShort(); }
    if (valueFormat & 0x0080) { valueRecord.yAdvDevice = undefined; this.parseShort(); }

    return valueRecord;
};

/**
 * Parse a list of GPOS valueRecords
 * https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#value-record
 * valueFormat and valueCount are read from the stream.
 */
Parser.prototype.parseValueRecordList = function() {
    const valueFormat = this.parseUShort();
    const valueCount = this.parseUShort();
    const values = new Array(valueCount);
    for (let i = 0; i < valueCount; i++) {
        values[i] = this.parseValueRecord(valueFormat);
    }
    return values;
};

Parser.prototype.parsePointer = function(description) {
    const structOffset = this.parseOffset16();
    if (structOffset > 0) {
        // NULL offset => return undefined
        return new Parser(this.data, this.offset + structOffset).parseStruct(description);
    }
    return undefined;
};

Parser.prototype.parsePointer32 = function(description) {
    const structOffset = this.parseOffset32();
    if (structOffset > 0) {
        // NULL offset => return undefined
        return new Parser(this.data, this.offset + structOffset).parseStruct(description);
    }
    return undefined;
};

/**
 * Parse a list of offsets to lists of 16-bit integers,
 * or a list of offsets to lists of offsets to any kind of items.
 * If itemCallback is not provided, a list of list of UShort is assumed.
 * If provided, itemCallback is called on each item and must parse the item.
 * See examples in tables/gsub.mjs
 */
Parser.prototype.parseListOfLists = function(itemCallback) {
    const offsets = this.parseOffset16List();
    const count = offsets.length;
    const relativeOffset = this.relativeOffset;
    const list = new Array(count);
    for (let i = 0; i < count; i++) {
        const start = offsets[i];
        if (start === 0) {
            // NULL offset
            // Add i as owned property to list. Convenient with assert.
            list[i] = undefined;
            continue;
        }
        this.relativeOffset = start;
        if (itemCallback) {
            const subOffsets = this.parseOffset16List();
            const subList = new Array(subOffsets.length);
            for (let j = 0; j < subOffsets.length; j++) {
                this.relativeOffset = start + subOffsets[j];
                subList[j] = itemCallback.call(this);
            }
            list[i] = subList;
        } else {
            list[i] = this.parseUShortList();
        }
    }
    this.relativeOffset = relativeOffset;
    return list;
};

///// Complex tables parsing //////////////////////////////////

// Parse a coverage table in a GSUB, GPOS or GDEF table.
// https://www.microsoft.com/typography/OTSPEC/chapter2.htm
// parser.offset must point to the start of the table containing the coverage.
Parser.prototype.parseCoverage = function() {
    const startOffset = this.offset + this.relativeOffset;
    const format = this.parseUShort();
    const count = this.parseUShort();
    if (format === 1) {
        return {
            format: 1,
            glyphs: this.parseUShortList(count)
        };
    } else if (format === 2) {
        const ranges = new Array(count);
        for (let i = 0; i < count; i++) {
            ranges[i] = {
                start: this.parseUShort(),
                end: this.parseUShort(),
                index: this.parseUShort()
            };
        }
        return {
            format: 2,
            ranges: ranges
        };
    }
    throw new Error('0x' + startOffset.toString(16) + ': Coverage format must be 1 or 2.');
};

// Parse a Class Definition Table in a GSUB, GPOS or GDEF table.
// https://www.microsoft.com/typography/OTSPEC/chapter2.htm
Parser.prototype.parseClassDef = function() {
    const startOffset = this.offset + this.relativeOffset;
    const format = this.parseUShort();
    if (format === 1) {
        return {
            format: 1,
            startGlyph: this.parseUShort(),
            classes: this.parseUShortList()
        };
    } else if (format === 2) {
        return {
            format: 2,
            ranges: this.parseRecordList({
                start: Parser.uShort,
                end: Parser.uShort,
                classId: Parser.uShort
            })
        };
    }

    console.warn(`0x${startOffset.toString(16)}: This font file uses an invalid ClassDef format of ${format}. It might be corrupted and should be reacquired if it doesn't display as intended.`);
    return {
        format: format
    };
};

///// Static methods ///////////////////////////////////
// These convenience methods can be used as callbacks and should be called with "this" context set to a Parser instance.

Parser.list = function(count, itemCallback) {
    return function() {
        return this.parseList(count, itemCallback);
    };
};

Parser.list32 = function(count, itemCallback) {
    return function() {
        return this.parseList32(count, itemCallback);
    };
};

Parser.recordList = function(count, recordDescription) {
    return function() {
        return this.parseRecordList(count, recordDescription);
    };
};

Parser.recordList32 = function(count, recordDescription) {
    return function() {
        return this.parseRecordList32(count, recordDescription);
    };
};

Parser.pointer = function(description) {
    return function() {
        return this.parsePointer(description);
    };
};

Parser.pointer32 = function(description) {
    return function() {
        return this.parsePointer32(description);
    };
};

Parser.tag = Parser.prototype.parseTag;
Parser.byte = Parser.prototype.parseByte;
Parser.uShort = Parser.offset16 = Parser.prototype.parseUShort;
Parser.uShortList = Parser.prototype.parseUShortList;
Parser.uInt24 = Parser.prototype.parseUInt24;
Parser.uLong = Parser.offset32 = Parser.prototype.parseULong;
Parser.uLongList = Parser.prototype.parseULongList;
Parser.fixed = Parser.prototype.parseFixed;
Parser.f2Dot14 = Parser.prototype.parseF2Dot14;
Parser.struct = Parser.prototype.parseStruct;
Parser.coverage = Parser.prototype.parseCoverage;
Parser.classDef = Parser.prototype.parseClassDef;

///// Script, Feature, Lookup lists ///////////////////////////////////////////////
// https://www.microsoft.com/typography/OTSPEC/chapter2.htm

const langSysTable = {
    reserved: Parser.uShort,
    reqFeatureIndex: Parser.uShort,
    featureIndexes: Parser.uShortList
};

Parser.prototype.parseScriptList = function() {
    return this.parsePointer(Parser.recordList({
        tag: Parser.tag,
        script: Parser.pointer({
            defaultLangSys: Parser.pointer(langSysTable),
            langSysRecords: Parser.recordList({
                tag: Parser.tag,
                langSys: Parser.pointer(langSysTable)
            })
        })
    })) || [];
};

Parser.prototype.parseFeatureList = function() {
    return this.parsePointer(Parser.recordList({
        tag: Parser.tag,
        feature: Parser.pointer({
            featureParams: Parser.offset16,
            lookupListIndexes: Parser.uShortList
        })
    })) || [];
};

Parser.prototype.parseLookupList = function(lookupTableParsers) {
    return this.parsePointer(Parser.list(Parser.pointer(function() {
        const lookupType = this.parseUShort();
        check.argument(1 <= lookupType && lookupType <= 9, 'GPOS/GSUB lookup type ' + lookupType + ' unknown.');
        const lookupFlag = this.parseUShort();
        const useMarkFilteringSet = lookupFlag & 0x10;
        return {
            lookupType: lookupType,
            lookupFlag: lookupFlag,
            subtables: this.parseList(Parser.pointer(lookupTableParsers[lookupType])),
            markFilteringSet: useMarkFilteringSet ? this.parseUShort() : undefined
        };
    }))) || [];
};

Parser.prototype.parseFeatureVariationsList = function() {
    return this.parsePointer32(function() {
        const majorVersion = this.parseUShort();
        const minorVersion = this.parseUShort();
        check.argument(majorVersion === 1 && minorVersion < 1, 'GPOS/GSUB feature variations table unknown.');
        const featureVariations = this.parseRecordList32({
            conditionSetOffset: Parser.offset32,
            featureTableSubstitutionOffset: Parser.offset32
        });
        return featureVariations;
    }) || [];
};

// VariationStore, ItemVariationStore, VariationRegionList, regionAxes, ItemVariationSubtables ...
// https://learn.microsoft.com/en-us/typography/opentype/spec/otvarcommonformats
// https://learn.microsoft.com/en-us/typography/opentype/spec/otvarcommonformats#item-variation-store-header-and-item-variation-data-subtables

Parser.prototype.parseVariationStore = function() {
    const vsOffset = this.relativeOffset;
    const length = this.parseUShort();
    const variationStore = {
        itemVariationStore: this.parseItemVariationStore()
    };
    this.relativeOffset = vsOffset + length + 2; // + 2 for length field
    return variationStore;
};

Parser.prototype.parseItemVariationStore = function() {
    const itemStoreOffset = this.relativeOffset;
    const iVStore = {
        format: this.parseUShort(),
        variationRegions: [],
        itemVariationSubtables: []
    };

    const variationRegionListOffset = this.parseOffset32();
    const itemVariationDataCount = this.parseUShort();
    const itemVariationDataOffsets = this.parseULongList(itemVariationDataCount);    
    
    this.relativeOffset = itemStoreOffset + variationRegionListOffset;
    iVStore.variationRegions = this.parseVariationRegionList();
    
    for( let i = 0; i < itemVariationDataCount; i++ ) {
        const subtableOffset = itemVariationDataOffsets[i];
        this.relativeOffset = itemStoreOffset + subtableOffset;
        iVStore.itemVariationSubtables.push(this.parseItemVariationSubtable());
    }

    return iVStore;
};

Parser.prototype.parseVariationRegionList = function() {
    const axisCount = this.parseUShort();
    const regionCount = this.parseUShort();
    return  this.parseRecordList(regionCount, {
        regionAxes: Parser.recordList(axisCount, {
            startCoord: Parser.f2Dot14,
            peakCoord: Parser.f2Dot14,
            endCoord: Parser.f2Dot14,
        })
    });
};

Parser.prototype.parseItemVariationSubtable = function() {
    const itemCount = this.parseUShort();
    const wordDeltaCount = this.parseUShort();

    const regionIndexes = this.parseUShortList();
    const regionIndexCount = regionIndexes.length;
    
    const subtable = {
        regionIndexes,
        deltaSets: itemCount && regionIndexCount ? this.parseDeltaSets(itemCount, wordDeltaCount, regionIndexCount) : []
    };

    return subtable;
};

Parser.prototype.parseDeltaSetIndexMap = function() {
    const format = this.parseByte();
    const entryFormat = this.parseByte();
    const map = [];
    let mapCount = 0;
    
    switch(format) {
        case 0:
            mapCount = this.parseUShort();
            break;
        case 1:
            mapCount = this.parseULong();
            break;
        default:
            console.error(`unsupported DeltaSetIndexMap format ${format}`);
    }

    if(!mapCount) return {
        format,
        entryFormat
    };
    
    const bitCount = (entryFormat & masks.INNER_INDEX_BIT_COUNT_MASK) + 1;
    const entrySize = ((entryFormat & masks.MAP_ENTRY_SIZE_MASK) >> 4) + 1;

    for (let n = 0; n < mapCount; n++) {
        let entry;
        if (entrySize === 1) {
            entry = this.parseByte();
        } else if (entrySize === 2) {
            entry = this.parseUShort();
        } else if (entrySize === 3) {
            entry = this.parseUInt24();
        } else if (entrySize === 4) {
            entry = this.getULong();
        } else {
            throw new Error(`Invalid entry size of ${entrySize}`);
        }
    
        const outerIndex = entry >> bitCount;
        const innerIndex = entry & ((1 << bitCount) - 1);
        map.push({ outerIndex, innerIndex });
    }

    return {
        format,
        entryFormat,
        map,
    };
};

Parser.prototype.parseDeltaSets = function(itemCount, wordDeltaCount, regionIndexCount) {
    const deltas = Array.from({length: itemCount},()=>[]); // two-dimensional array with empty rows and columns

    const longFlag = wordDeltaCount & masks.LONG_WORDS;
    const wordCount = wordDeltaCount & masks.WORD_DELTA_COUNT_MASK;

    if(wordCount > regionIndexCount) {
        throw Error('wordCount must be less than or equal to regionIndexCount');
    }
    
    const wordParser = (longFlag ? this.parseLong : this.parseShort).bind(this);
    const restParser = (longFlag ? this.parseShort : this.parseChar).bind(this);
    
    for (let i = 0; i < itemCount; i++) {
        for (let j = 0; j < regionIndexCount; j++) {
            if (j < wordCount) {
                deltas[i].push(wordParser());
            } else {
                deltas[i].push(restParser());
            }
        }
    }

    return deltas;
};

Parser.prototype.parseTupleVariationStoreList = function(axisCount, flavor, glyphs) {
    const glyphCount = this.parseUShort();
    const flags = this.parseUShort();
    const offsetSizeIs32Bit = flags & 0x01;

    const glyphVariationDataArrayOffset = this.parseOffset32();
    const parseOffset = (offsetSizeIs32Bit ? this.parseULong : this.parseUShort).bind(this);

    const glyphVariations = {};

    let currentOffset = parseOffset();
    if (!offsetSizeIs32Bit) currentOffset *= 2;
    let nextOffset;

    for (let i = 0; i < glyphCount; i++) {
        nextOffset = parseOffset();
        if (!offsetSizeIs32Bit) nextOffset *= 2;
        
        const length = nextOffset - currentOffset;

        glyphVariations[i] = length
            ? this.parseTupleVariationStore(
                glyphVariationDataArrayOffset + currentOffset,
                axisCount,
                flavor,
                glyphs,
                i
            )
            : undefined;
        
        currentOffset = nextOffset;
    }

    return glyphVariations;
};

Parser.prototype.parseTupleVariationStore = function(tableOffset, axisCount, flavor, glyphs, glyphIndex) {
    const relativeOffset = this.relativeOffset;
    
    this.relativeOffset = tableOffset;
    if(flavor === 'cvar') {
        this.relativeOffset+= 4; // we already parsed the version fields in cvar.mjs directly
    }

    // header
    const tupleVariationCount = this.parseUShort();
    const hasSharedPoints = !!(tupleVariationCount & masks.SHARED_POINT_NUMBERS);
    // const reserved = tupleVariationCount & 0x7000;
    const count = tupleVariationCount & masks.COUNT_MASK;
    let dataOffset = this.parseOffset16();
    const headers = [];
    let sharedPoints = [];
    
    for(let h = 0; h < count; h++) {
        const headerData = this.parseTupleVariationHeader(axisCount, flavor);
        headers.push(headerData);
    }
    
    if (this.relativeOffset !== tableOffset + dataOffset) {
        console.warn(`Unexpected offset after parsing tuple variation headers! Expected ${tableOffset + dataOffset}, actually ${this.relativeOffset}`);
        this.relativeOffset = tableOffset + dataOffset;
    }    

    if (hasSharedPoints) {
        sharedPoints = this.parsePackedPointNumbers();
    }

    let serializedDataOffset = this.relativeOffset;

    for(let h = 0; h < count; h++) {
        const header = headers[h];
        header.privatePoints = [];
        this.relativeOffset = serializedDataOffset;
        
        if(flavor === 'cvar' && !header.peakTuple) {
            console.warn('An embedded peak tuple is required in TupleVariationHeaders for the cvar table.');
        }

        if(header.flags.privatePointNumbers) {
            header.privatePoints = this.parsePackedPointNumbers();
        }
        delete header.flags; // we don't need to expose this
        
        const deltasOffset = this.offset;
        const deltasRelativeOffset = this.relativeOffset;

        const defineDeltas = (propertyName) => {
            let _deltas = undefined;
            let _deltasY = undefined;

            const parseDeltas = () => {
                let pointsCount = 0;
                if(flavor === 'gvar') {
                    pointsCount = header.privatePoints.length || sharedPoints.length;
                    if(!pointsCount) {
                        const glyph = glyphs.get(glyphIndex);
                        // make sure the path is available
                        glyph.path;
                        pointsCount = glyph.points.length;
                        // add 4 phantom points, see https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructing_glyphs#phantoms
                        // @TODO: actually generate these points from glyph.getBoundingBox() and glyph.getMetrics(),
                        // as they may be influenced by variation as well
                        pointsCount+= 4;
                    }
                } else if (flavor === 'cvar') {
                    pointsCount = glyphs.length; // glyphs here is actually font.tables.cvt
                }
                
                this.offset = deltasOffset;
                this.relativeOffset = deltasRelativeOffset;
                _deltas = this.parsePackedDeltas(pointsCount);
                
                if(flavor === 'gvar') {
                    _deltasY = this.parsePackedDeltas(pointsCount);
                }
            };

            return {
                configurable: true,
        
                get: function() {
                    if(_deltas === undefined) parseDeltas();
                    return propertyName === 'deltasY' ? _deltasY : _deltas;
                },
        
                set: function(deltas) {
                    if(_deltas === undefined) parseDeltas();
                    if(propertyName === 'deltasY') {
                        _deltasY = deltas;
                    } else {
                        _deltas = deltas;
                    }
                }
            };
        };

        Object.defineProperty(header, 'deltas', defineDeltas.call(this, 'deltas'));
        if(flavor === 'gvar') {
            Object.defineProperty(header, 'deltasY', defineDeltas.call(this, 'deltasY'));
        }

        serializedDataOffset += header.variationDataSize;
        delete header.variationDataSize; // we don't need to expose this
    }

    this.relativeOffset = relativeOffset;
    const result = {
        headers,
    };

    result.sharedPoints = sharedPoints;

    return result;
};

Parser.prototype.parseTupleVariationHeader = function(axisCount, flavor) {
    const variationDataSize = this.parseUShort();
    const tupleIndex = this.parseUShort();

    const embeddedPeakTuple = !!(tupleIndex & masks.EMBEDDED_PEAK_TUPLE);
    const intermediateRegion = !!(tupleIndex & masks.INTERMEDIATE_REGION);
    const privatePointNumbers = !!(tupleIndex & masks.PRIVATE_POINT_NUMBERS);
    // const reserved = tupleIndex & 0x1000;
    const sharedTupleRecordsIndex = embeddedPeakTuple ? undefined : tupleIndex & masks.TUPLE_INDEX_MASK;

    const peakTuple = embeddedPeakTuple ? this.parseTupleRecords(1, axisCount)[0] : undefined;
    const intermediateStartTuple = intermediateRegion ? this.parseTupleRecords(1, axisCount)[0] : undefined;
    const intermediateEndTuple = intermediateRegion ? this.parseTupleRecords(1, axisCount)[0] : undefined;

    const result = {
        variationDataSize,
        peakTuple,
        intermediateStartTuple,
        intermediateEndTuple,
        flags: {
            embeddedPeakTuple,
            intermediateRegion,
            privatePointNumbers,
        }
    };

    if(flavor === 'gvar') {
        result.sharedTupleRecordsIndex = sharedTupleRecordsIndex;
    }

    return result;
};

Parser.prototype.parsePackedPointNumbers = function() {
    const countByte1 = this.parseByte();
    const points = [];
    let totalPointCount = countByte1;
    
    if (countByte1 >= 128) {
        // High bit is set, need to read a second byte and combine.
        const countByte2 = this.parseByte();
    
        // Combine as big-endian uint16, with high bit of the first byte cleared.
        // This is done by masking the first byte with 0x7F (to clear the high bit)
        // and then shifting it left by 8 bits before adding the second byte.
        totalPointCount = ((countByte1 & masks.POINT_RUN_COUNT_MASK) << 8) | countByte2;
    }
    
    let lastPoint = 0;
    while (points.length < totalPointCount) {
        const controlByte = this.parseByte();
        const numbersAre16Bit = !!(controlByte & masks.POINTS_ARE_WORDS); // Check if high bit is set
        let runCount = (controlByte & masks.POINT_RUN_COUNT_MASK) + 1; // Number of points in this run
        for (let i = 0; i < runCount && points.length < totalPointCount; i++) {
            let pointDelta;
            if (numbersAre16Bit) {
                pointDelta = this.parseUShort(); // Parse delta as uint16
            } else {
                pointDelta = this.parseByte(); // Parse delta as uint8
            }
            // For the first point of the first run, use the value directly. Otherwise, accumulate.
            lastPoint = lastPoint + pointDelta;
            points.push(lastPoint);
        }
    }

    return points;
};

Parser.prototype.parsePackedDeltas = function(expectedCount) {
    const deltas = [];
    
    while (deltas.length < expectedCount) {
        const controlByte = this.parseByte();
        const zeroData = !!(controlByte & masks.DELTAS_ARE_ZERO);
        const deltaWords = !!(controlByte & masks.DELTAS_ARE_WORDS);
        const runCount = (controlByte & masks.DELTA_RUN_COUNT_MASK) + 1;
        
        for (let i = 0; i < runCount && deltas.length < expectedCount; i++) {
            if(zeroData) {
                deltas.push(0);
            } else if (deltaWords) {
                deltas.push(this.parseShort());
            } else {
                deltas.push(this.parseChar());
            }
        }
    }

    return deltas;
};

export default {
    getByte,
    getCard8: getByte,
    getUShort,
    getCard16: getUShort,
    getShort,
    getUInt24,
    getULong,
    getFixed,
    getTag,
    getOffset,
    getBytes,
    bytesToString,
    Parser,
};

export { Parser };
